/*  

Cafechess Library: Cover basic chess programming needs, like GUI, Engine integration, 
FEN and PGN parsing.

                 \\|//
                 (o o)
--------------ooO-(_)-Ooo----------------------------------
Copyright (C) 2009  By Gregory A. Prentice Snoqualmie, WA
                    www.cafechess.org
                    cafechess@comcast.net
                      Oooo
--------------oooO----(  )---------------------------------
              (  )    (_/
               \_)

Microsoft Public License (Ms-PL)
               
Revisions:

Author        Date    Description:

*/
using System;
using System.Text.RegularExpressions;
using System.Collections;
using System.IO;

namespace Cafechess.Chess.Parsers
{
  /// <summary>
  /// Provides the parsing of the PGN standard game notation files
  /// as defined by the standard.
  /// </summary>
  public class PGNparser : IGameParser
  {
    Regex coRegex;
    public delegate void newGame(IGameParser iParser);
    public event newGame EventNewGame;
    public delegate void exitHeader(IGameParser iParser);
    public event exitHeader EventExitHeader;
    public delegate void enterVariation(IGameParser iParser);
    public event enterVariation EventEnterVariation;
    public delegate void exitVariation(IGameParser iParser);
    public event exitVariation EventExitVariation;
    public delegate void starting(IGameParser iParser);
    public event starting EventStarting;
    public delegate void finished(IGameParser iParser);
    public event finished EventFinished;
    public delegate void tagParsed(IGameParser iParser);
    public event tagParsed EventTagParsed;
    public delegate void nagParsed(IGameParser iParser);
    public event nagParsed EventNagParsed;
    public delegate void moveParsed(IGameParser iParser);
    public event moveParsed EventMoveParsed;
    public delegate void commentParsed(IGameParser iParser);
    public event commentParsed EventCommentParsed;
    public delegate void endMarkerParsed(IGameParser iParser);
    public event endMarkerParsed EventendMarkerParsed;

    string coData;
    System.Text.StringBuilder coValue;
    bool coNextGame;
    int coPeriodCount;

    /// <summary>
    /// Saves the state of the parser as it enter into a variation.
    /// </summary>
    Stack coSaveState;

    /// <summary>
    /// Allows access to the current state of the parser
    /// when an event has been fired.
    /// </summary>
    public GameParserState.State State
    {
      get{ return coState; }
      set{ coState = value;}
    }
    GameParserState.State coState;
    GameParserState.State coPrevState;
    /// <summary>
    /// Contains the PGN tag information.
    /// </summary>
    public string Tag
    {
      get{ return coTag; }
    }
    string coTag;
    /// <summary>
    /// Contains the values currently parsed, normally this
    /// is accessed from the event listeners as the parses
    /// signals it has found something.
    /// </summary>
    public string Value
    { 
      get{ return coValue.ToString(); }    
    }
    
    /// <summary>
    /// File name to open and parse.
    /// </summary>
    public string Filename
    {
      get{ return coFilename;}
      set{ coFilename=value;}
    }
    string coFilename;

    /// <summary>
    /// Constructor the initializes our parser.
    /// </summary>
    public PGNparser()
    {
      coRegex = new Regex("^\\[([A-Za-z]*) \"(.*)\"",RegexOptions.Compiled);
      coValue = new System.Text.StringBuilder();
      coState = GameParserState.State.HEADER;
      coSaveState = new Stack();
      coPeriodCount = 0;
    }

    /// <summary>
    /// Adds a specific listner to the events fired by the parsing
    /// of the PGN data.
    /// </summary>
    /// <param name="ievents"></param>
    public void AddEvents(IGameParserEvents ievents)
    {
      EventNewGame += new newGame(ievents.NewGame);
      EventExitHeader +=new exitHeader(ievents.ExitHeader);
      EventEnterVariation += new enterVariation(ievents.EnterVariation);
      EventExitVariation += new exitVariation(ievents.ExitVariation);
      EventStarting +=new starting(ievents.Starting);
      EventFinished +=new finished(ievents.Finished);
      EventTagParsed +=new tagParsed(ievents.TagParsed);
      EventNagParsed +=new nagParsed(ievents.NagParsed);
      EventMoveParsed +=new moveParsed(ievents.MoveParsed);
      EventCommentParsed += new commentParsed(ievents.CommentParsed);
      EventendMarkerParsed += new endMarkerParsed(ievents.EndMarker);
      
    }

    /// <summary>
    /// Remove a specific listner from the events fired by the parsing
    /// of the PGN data.
    /// </summary>
    /// <param name="ievents"></param>
    public void RemoveEvents(IGameParserEvents ievents)
    {
      EventNewGame -= new newGame(ievents.NewGame);
      EventExitHeader +=new exitHeader(ievents.ExitHeader);
      EventEnterVariation -= new enterVariation(ievents.EnterVariation);
      EventExitVariation -= new exitVariation(ievents.ExitVariation);
      EventStarting +=new starting(ievents.Starting);
      EventFinished -=new finished(ievents.Finished);
      EventTagParsed -=new tagParsed(ievents.TagParsed);
      EventNagParsed -=new nagParsed(ievents.NagParsed);
      EventMoveParsed -=new moveParsed(ievents.MoveParsed);
      EventCommentParsed -= new commentParsed(ievents.CommentParsed);
      EventendMarkerParsed -= new endMarkerParsed(ievents.EndMarker);
    }

    /// <summary>
    /// Responsible for the main driver loop of the parser.  Here we read
    /// in the PGN file and produce events for the listening parties.
    /// </summary>
    public void Parse()
    {
      System.Text.StringBuilder builder = new System.Text.StringBuilder(1024);
      FileStream fs=null;
      BinaryReader reader = null;

      if( EventStarting != null )
        EventStarting(this);
      try
      {
        fs = new FileStream(coFilename, FileMode.Open, FileAccess.Read);
        reader = new BinaryReader(fs);

        long bytesread =0;
        while(bytesread != fs.Length)
        {
          byte aByte = reader.ReadByte();
          switch(aByte)
          {
            case 0x0D:
              break;
            case 0x0A:
              coData = builder.ToString();
              builder.Length = 0;
              if( coData.Length > 0 )
              {
                // Check to make sure we're not inside a comment section then
                //  see if we are at a game tag.
                if( coState != GameParserState.State.COMMENT && 
                  Regex.IsMatch(coData,"^\\[") )
                {
                  if( coNextGame == false )
                  {

                    CallEvent(coState);

                    coNextGame = true;
                    if( EventNewGame != null )
                    {
                      EventNewGame(this);
                    }
                  }
                  coState = GameParserState.State.HEADER;
                  ParseTag(coData);
                  coValue.Length = 0;
                }
                else
                {
                  if( coNextGame )
                  {
                    coNextGame = false;
                    if( EventExitHeader != null )
                    {
                      EventExitHeader(this);
                    }
                  }            
                  ParseDetail(coData);
                }
              }
              break;
            default:
              builder.Append((char)aByte);
              break;
          }
          bytesread++;
        }

        CallEvent(coState);
      }
      catch(ApplicationException)
      {
        throw;
      }
      finally
      {
        if( reader != null )
        {
          reader.Close();
          reader = null;          
        }
      }
      if( EventFinished != null )
        EventFinished(this);
    }

    /// <summary>
    /// Handles the parsing of the actual game notation of the PGN file.
    /// </summary>
    /// <param name="line"></param>
    public void ParseDetail(string line)
    {
      foreach(char aChar in line)
      {
        // Handle any special processing of our text.
        switch(coState)
        {
          case GameParserState.State.COMMENT:
            if( aChar == '}' )
            {
              CallEvent(coState);
            }
            else
              coValue.Append(aChar);
            break;

          case GameParserState.State.NAGS:            
            if( aChar >= '0' && aChar <= '9' )
              coValue.Append(aChar);                              
            else
            {
              CallEvent(coState);
              HandleChar(aChar);
            }            
            break;
          case GameParserState.State.COLOR:
            if( aChar == '.' )
            {
              coPeriodCount++;
            }
            else
            {
              coValue.Length = 0;
              if( coPeriodCount == 1 )
              {
                coState = GameParserState.State.WHITE;
              }
              else if( coPeriodCount > 1 )
              {
                coState = GameParserState.State.BLACK;
              }
              HandleChar(aChar);
              coPeriodCount=0;
            }
            break;

          default:
            HandleChar(aChar);
            break;
        }
      }
      // Ensure we add a space between comment lines that are broken appart.
      if( coState == GameParserState.State.COMMENT )
        coValue.Append( ' ' );
      else
        CallEvent(coState);
    }

    /// <summary>
    /// Handles individual charaters which may signal a change in the
    /// parser's state.
    /// </summary>
    /// <param name="aChar"></param>
    void HandleChar(char aChar)
    {
      switch( aChar )
      {
        case '{':
          CallEvent(coState);
          coPrevState = coState;
          coState = GameParserState.State.COMMENT;
          break;
        case '(':
          if( EventEnterVariation != null )
            EventEnterVariation(this);
          coValue.Length = 0;
          coSaveState.Push(coState);
          break;
        case ')':
          if( EventExitVariation != null )
            EventExitVariation(this);
          coValue.Length = 0;
          coState = (GameParserState.State) coSaveState.Pop();
          break;
        case ' ':
          // Only if we have some data do we want to fire an event.
          CallEvent(coState);
          break;
        case '.':
          // We may have come across 6. e4 6... d5 as in our example data.
          coState = GameParserState.State.NUMBER;
          CallEvent(coState);
          coPeriodCount=1;
          break;
        case '$':
          CallEvent(coState);
          coPrevState = coState;
          coState = GameParserState.State.NAGS;
          break;
        case '!':
        case '?':
          if( coState != GameParserState.State.CONVERT_NAG )
          {
            CallEvent(coState);          
            coPrevState = coState;
            coState = GameParserState.State.CONVERT_NAG;
          }
          coValue.Append(aChar);
          break;
        case '-':
          if(coState != GameParserState.State.ENDMARKER &&
             coValue.Length >= 1 )
          {
            if( "012".IndexOf(coValue[coValue.Length-1]) >= 0 )
              coState = GameParserState.State.ENDMARKER;
          }
          coValue.Append(aChar);
          break;
        case '*':
          coState = GameParserState.State.ENDMARKER;
          coValue.Append(aChar);
          break;
        case '\t':
          break;
        default:
          coValue.Append(aChar);
          break;
      }
    }

    /// <summary>
    /// Calls the correct event based on the parsers state.
    /// </summary>
    /// <param name="state"></param>
    void CallEvent(GameParserState.State state)
    {
      if( coValue.Length > 0 )
      {
        switch(state)
        {
          case GameParserState.State.COMMENT:
            if( EventCommentParsed != null )
              EventCommentParsed(this);
            coState = coPrevState;
            break;
          case GameParserState.State.NAGS:
            if( EventNagParsed != null )
              EventNagParsed(this);
            coState = coPrevState;
            break;
          case GameParserState.State.CONVERT_NAG:
            string nag = coValue.ToString();
            coValue.Length=0;
            if( nag.Equals("!" ) )
              coValue.Append('1');
            else if( nag.Equals("?" ) )
              coValue.Append('2');
            else if( nag.Equals("!!" ) )
              coValue.Append('3');
            else if( nag.Equals("??" ) )
              coValue.Append('4');
            else if( nag.Equals("!?" ) )
              coValue.Append('5');
            else if( nag.Equals("?!" ) )
              coValue.Append('6');
            else
              coValue.Append('0');
            if( EventNagParsed != null )
              EventNagParsed(this);
            coState = coPrevState;
            break;
          case GameParserState.State.NUMBER:
            if( EventMoveParsed != null )
              EventMoveParsed(this);
            coState = GameParserState.State.COLOR;
            break;
          case GameParserState.State.WHITE:
            if( EventMoveParsed != null )
              EventMoveParsed(this);
            coState = GameParserState.State.BLACK;
            break;
          case GameParserState.State.BLACK:
            if( EventMoveParsed != null )
              EventMoveParsed(this);         
            coState = GameParserState.State.NUMBER;
            break;
          case GameParserState.State.ENDMARKER:
            if( EventendMarkerParsed != null )
              EventendMarkerParsed(this);         
            coState = GameParserState.State.HEADER;
            break; 
        }
      }
      // Always clear out our data as the handler should have used this value during the event.
      coValue.Length = 0;
    }

    /// <summary>
    /// Parses out the PGN tag and value from the game header.
    /// </summary>
    /// <param name="line"></param>
    public void ParseTag(string line)
    {
      int nleft = line.IndexOf('"');
      int nright = line.LastIndexOf('"');

      System.Text.RegularExpressions.Match regMatch = coRegex.Match(line);
      if( regMatch.Groups.Count == 3 )
      {
        // Call the events with the tag and tag value.
        if( EventTagParsed != null )
        {
          coTag = regMatch.Groups[1].Value;
          coValue.Length = 0;
          coValue.Append(regMatch.Groups[2].Value);
          EventTagParsed(this);
        }
      }
    }

  }
}
